使用新 Android Gradle 插件加速您的应用构建
△ 使用新 Android Gradle 插件加速您的应用构建
Bilibili 视频链接
https://www.bilibili.com/video/BV1Tq4y1e77K/
Gradle 的性能改进
Kotlin 符号处理优化
Kotlin 符号处理 (Kotlin Symbol Processing,简称 KSP) 是 kapt (Kotlin annotation processing tool) 的替代品,它为 Kotlin 语言带来了一流的注解处理能力,处理速度最快可以达到 kapt 的两倍。目前已经有不少知名的软件库提供了兼容 KSP 的注解处理器,比如 Room、Moshi、Kotishi 等等。因此我们建议,当您的应用中所用到的各种注解处理器都支持 KSP 时,应该尽快从 kapt 迁移到 KSP。
KSP
https://github.com/google/ksp
非传递性 R 类
启用非传递性 R 类 (non-transitive R-class) 后,您应用中的 R 类将只会包含在子项目中声明的资源,依赖项中的资源会被排除在外。这样一来,子项目中的 R 类大小将会显著减少。
这一改动可以在您向运行时依赖项中添加新资源时,避免重新编译下游模块。在这种场景下,可以给您的应用带来 40% 的性能提升。另外,在清理构建产物时,我们发现性能有 5% 到 10% 的改善。
您可以在 gradle.properties 文件中添加下面的标记:
android.nonTransitiveRClass=true
您也可以在 Android Studio Arctic Fox 及以上版本使用重构工具来启用非传递性 R 类,具体需要您运行 Android Studio 菜单栏的 Refactor --> Migrate to Non-transitive R Classes。这种方法还可以在必要时帮助您修改相关源代码。目前,AndroidX 库已经启用此特性,因此 AAR 阶段的产物中将不再包含来自传递性依赖项的资源。
Lint 性能优化
从 Android Gradle 插件 7.0 版本开始,Lint 任务可以显示为 "UP-TO-DATE",即如果模块的源代码和资源没有更改,那么就不需要对该模块进行 Lint 分析任务。您需要在 build.gradle 中添加选项:
// build.gradle
android {
...
lintOptions {
checkDependencies true
}
}
UP-TO-DATE
https://developer.android.google.cn/studio/releases/gradle-plugin#lint_tasks_can_now_be_up-to-date
从 Android Gradle 插件的 7.1.0-alpha 13 版本开始,Lint 分析任务兼容了 Gradle 构建缓存 (Gradle build cache),它可以通过复用其他构建的结果来减少新构建的时间:
△ 不同 AGP 版本中 Lint 时间比较
我们在一个演示项目中开启了 Gradle 构建缓存并设置 checkDependencies 为 true,然后分别使用 AGP 4.2、7.0 和 7.1 进行构建。从上图中可看出,7.0 版本的构建速度是 4.2 的两倍;并且在使用 AGP 7.1 时,由于所有 Lint 分析任务都命中了缓存而带来了更加显著的速度提升。
您不但可以直接通过更新 Android Gradle 插件版本获得更好的 Lint 性能,还能通过一些配置来进一步提升效率。其中一种方法是使用可缓存的 Lint 分析任务。要启用 Gradle 的构建缓存,您需要在 gradle.properties 文件中开启下面的标记 (参见 Build Cache):
https://docs.gradle.org/current/userguide/build_cache.html
org.gradle.caching=true
△ 在 gradle.properties 中开启 Gradle 构建缓存
另一种可改进 Lint 分析任务性能的方法是,在您条件允许的情况下给 Lint 分配更多的内存。
同时,我们建议您在应用模块的 Gradle 配置中为 lintOptions 块添加:
checkDependencies true
△ 在模块的 build.gradle 中添加 checkDependencies 标记
虽然这样不能让 Lint 分析任务更快执行,但能够让 Lint 在分析您指定应用时捕捉到更多问题,并且为整个项目生成一份 Lint 报告。
Gradle 配置缓存
△ Gradle 构建过程和阶段划分
每当 Gradle 开始构建时,它都会创建一个任务图用于执行构建操作。我们称这个过程为配置阶段 (configuration phase),它通常会持续几秒到数十秒。Gradle 配置缓存可以将配置阶段的输出进行缓存,并且在后续构建中复用这些缓存。当配置缓存命中,Gradle 会并行执行所有需要构建的任务。再加上依赖解析的结果也被缓存了,整个 Gradle 构建的过程变得更加快速。
这里需要说明,Gradle 配置缓存和构建缓存是不同的,后者缓存的是构建任务的产物。
在构建过程中,您的构建设置决定了构建阶段的结果。所以配置缓存会将诸如 gradle.properties、构建文件等输入捕获,放入缓存中。这些内容同您请求构建的任务一起,唯一地确定了在构建中要执行的任务。
接下来,结合代码,一探配置缓存的工作原理:
project.tasks.register("mytask", MyTask).configure {
it.classes.from(project.configurations.getByName("compileClasspath"))
it.name.set(project.name)
}
△ 配置缓存工作原理示例
abstract class GetGitShaTask extends DefaultTask {
@OutputFile File getOutputFile() { return new File(project.buildDir, "sha.txt") }
@TaskAction void process() {
def stdout = new ByteArrayOutputStream()
project.exec {
it.commandLine("git", "rev-parse", "HEAD")
standardOutput = stdout
}
getOutputFile().write(stdout.toString())
}
}
project.tasks.register("myTask", GetGitShaTask)
abstract class GetGitShaTask extends DefaultTask {
@OutputFile abstract RegularFileProperty getOutputFile()
@javax.inject.Inject abstract ExecOperations getExecOperations()
@TaskAction void process() {
def stdout = new ByteArrayOutputStream()
getExecOperations().exec {
// ...
}
getOutputFile().get().asFile.write(stdout.toString())
}
}
project.tasks.register("myTask", GetGitShaTask) {
getOutputFile().set(
project.layout.buildDirectory.file("sha.txt")
)
}
△ 使用 Gradle 服务注入来执行外部进程 (与配置缓存兼容的构建任务例子)
您可以从新代码发现,我们在任务注册期间,将输出文件的位置捕获并存入了某个属性中,然后通过注入的 Gradle 服务来执行 git 命令并获得命令的输出信息。这段代码还有另外一个好处,由于 Gradle 的延迟属性是实际使用时才计算的,所以 buildDirectory 发生的变动会自动反映在任务的输出文件位置上。
关于 Gradle 配置缓存和如何迁移您的构建任务的更多信息,请参阅:
Gradle 文档
https://docs.gradle.org/current/userguide/configuration_cache.html
扩展 Android Gradle 插件
不少开发者都发现在自己的构建任务中,有一些操作是无法通过 Android Gradle 插件直接实现的。所以接下来我们会着重探讨如何通过 AGP 新增的 Variant 和 Artifact API 来实现这些功能。
△ Android Gradle 插件的执行结构
build 类型 (buildTypes) 和产品变种 (productFlavors) 都是您项目的 build.gradle 文件中的概念。Android Gradle 插件会根据您的这些定义生成不同的变体对象,并对应各自的构建任务。这些构建任务的输出会被注册为与任务对应的工件 (artifact),并且根据需要被分为公有工件和私有工件。早期版本的 AGP API 允许您访问这些构建任务,但是这些 API 并不稳健,因为每个任务的具体实现细节是会发生改变的。Android Gradle 插件在 7.0 版本中引入了新的 API,让您可以访问到这些变体对象和一些中间工件。这样一来,开发者就可以在不操作构建任务的前提下改变构建行为。
修改构建时产生的工件
在这个部分,我们要通过修改 asset 的工件来向 APK 添加额外的 asset,代码如下:
// buildSrc/src/main/kotlin/AddAssetTask.kt
abstract class AddAssetTask: DefaultTask() {
@get:Input
abstract val content: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun taskAction() {
File(outputDir.asFile.get(), "extra.txt").writeText(content.get())
}
}
△ 向 APK 添加额外的 asset
// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
override fun apply(project: Project) {
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
val taskProvider =
project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
it.content.set("foo")
}
// 核心部分
variant.artifacts
.use(taskProvider)
.wireWith(AddAssetTask::outputDir)
.toAppendTo(MultipleArtifact.ASSETS)
}
}
}
△ 将 AddAssetTask 实例连接到对应的工件
上述代码中的核心部分会将任务的输出目录添加到 asset 目录的集合中,并正确连接任务依赖项。这段代码中我们将额外 asset 的内容硬编码为 "foo",但后面的步骤我们会对这里进行更改,还请您阅读时留意。
androidComponents.onVariants { variant ->
val aar: RegularFileProperty = variant.artifacts.get(AAR)
}
△ 获取 AAR 工件
Variant API、工件和任务
https://developer.android.google.cn/studio/build/extend-agp#variant-api-artifacts-tasks
修改和扩展 DSL
接下来我们需要修改 Android Gradle 插件的 DSL,从而允许我们设置额外 asset 的内容。新版本的 Android Gradle 插件允许您为自定义插件编写额外的 DSL 内容,所以我们会用这种方式来编辑每个构建类型的额外 asset。下面的代码展示了我们对模块的 build.gradle 文件的修改。
// app/build.gradle
android {
...
buildTypes {
release {
toy
content = "Hello World"
}
}
}
}
△ 在 build.gradle 中添加自定义 DSL
另外,为了能够扩展 Android Gradle 插件的 DSL,我们需要创建一个简单的接口。您可以参照下面一段代码:
// buildSrc/src/main/kotlin/ToyExtension.kt
interface ToyExtension {
var content: String?
}
定义好接口之后,我们需要为每一个 build 类型添加新定义的扩展:
// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
override fun apply(project: Project) {
val android = project.extensions.getByType(ApplicationExtension::class.java)
android.buildTypes.forEach {
it.extensions.add("toy", ToyExtension::class.java)
}
// ...
}
}
△ 为所有 build 类型添加新定义的扩展
您也可以使用自定义接口扩展产品变种,不过在这个例子中我们不需要这样做。我们还需要对 ToyPlugin.kt 作进一步修改,让插件可以获取到我们在 DSL 中为每个变体定义的 asset 内容:
// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
override fun apply(project: Project) {
// ...
// 注意这里省略了上一段代码增加的内容
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.onVariants { variant ->
val buildType = android.buildTypes.getByName(variant.buildType)
val toyExtension = buildType.extensions.findByName("toy") as? ToyExtension
val content = toyExtension?.content ?: "foo"
val taskProvider =
project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
it.content.set(content)
}
// 注意这里省略了修改工件的部分
// ...
}
}
}
上述代码中,我们增加了一段代码用于获取新增的 toyExtension 定义的内容,也就是刚才修改 DSL 时为每个 build 类型定义的额外 asset。需要您注意,我们这里定义了备选 asset 内容,也就是当您没有为某个 build 类型定义 asset 时,会默认使用的值。
使用 Variant API 添加自定义属性
您还可以用类似扩展 DSL 的方法来扩展 Variant API,具体来说就是向 Android Gradle 插件的 Variant 对象中添加您自己的 Gradle 属性或某种 Gradle Provider。相比仅扩展 DSL,扩展 Variant API 有这样一些优势:
DSL 值是固定的,但自定义变体属性可以使用构建任务的输出,Gradle 会自动处理所有构建任务的依赖项。 您可以很方便地为每个变体的自定义变体属性设置独立的值。 与自定义 DSL 相比,自定义变体属性能提供与其他插件之间更简单、稳健的交互。
// buildSrc/src/main/kotlin/ToyVariantExtension.kt
interface ToyVariantExtension {
val content: Property<String>
}
// 比较之前的 ToyExtension (您不需要在代码中包括这部分)
interface ToyExtension {
val content: String?
}
△ 定义带有自定义变体属性的扩展 (对比普通扩展)
通过与先前的 ToyExtension 定义对比,您会注意到我们使用了 Property 而不是可空字符串类型。这样做是为了与 Android Gradle 插件内部的代码习惯保持一致,既能支持您将任务的输出作为自定义属性的值,又避免您再去考虑复杂的插件排序过程。其他插件也可以设置属性值,至于发生在 Toy 插件之前还是之后都没有影响。下面的代码展示了使用自定义属性的方式:
// app/build.gradle
androidComponents {
onVariants(
selector().all(),
{ variant ->
variant.getExtension(ToyVariantExtension.class)
?.content
?.set("Hello ${variant.name}")
}
)
}
虽然这样的写法没有直接扩展 DSL 那样简单,但它可以很方便地为每个变体设置自定义属性的值。相应的,还需要修改 ToyPlugin.kt 文件:
// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
override fun apply(project: Project) {
// ...
// 注意这里省略了部分内容
val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
androidComponents.beforeVariants { variantBuilder ->
val buildType = android.buildTypes.getByName(variantBuilder.buildType)
val toyExtension = buildType.extensions.findByName("toy") as? ToyExtension
val variantExtension = project.objects.newInstance(ToyVariantExtension::class.java)
variantExtension.content.set(toyExtension?.content ?: "foo")
variantBuilder.registerExtension(ToyVariantExtension::class.java, variantExtension)
// 注意这里省略了部分内容
// ...
}
}
}
△ 注册带有自定义变体属性的 AGP 扩展
// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {
override fun apply(project: Project) {
// ...
// 注意这里省略了上一段展示内容
androidComponents.onVariants { variant ->
val content = variant.getExtension(VariantExtension::class.java)?.content
val taskProvider =
project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {
it.content.set(content)
}
// 注意这里省略了修改工件的部分
// ...
}
}
}
上面这段代码很好地展示了使用自定义变体属性的优势,特别是当您有多个需要以变体专用的方式进行交互的插件时更是如此。如果其他插件也想设置您的自定义变体属性,或者将属性用于它们的构建任务,也只需要使用类似上述 onVariants 代码块的方式。
如果您想要了解更多关于扩展 Android Gradle 插件的内容,敬请关注我们的 Gradle 与 AGP 构建 API 系列文章。您也可以阅读 Android 开发者文档: 扩展 Android Gradle 插件或者研读 GitHub 上的 AGP Cookbook。在不久的将来,我们还会推出更多构建和同步方面的改进,敬请关注。
文档: 扩展 Android Gradle 插件
https://developer.android.google.cn/studio/build/extend-agpAGP Cookbook
https://github.com/android/gradle-recipes
下一步工作
Project Isolation
了解 Project Isolation
https://gradle.github.io/configuration-cache/#project_isolation
改进 Kotlin 增量编译
推荐阅读